unit database_troops;

{$mode objfpc}{$H+}
{$modeswitch nestedprocvars}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls,
  Buttons, database_base, itemselectorframeunit, battleeventframeunit, fpjson,
  BGRABitmap, BGRABitmapTypes, fgl;

type

  TSelectedTroopMemberData = record
    IsActive: Boolean;
    MemberId: Integer;
    ShiftX: Integer;
    ShiftY: Integer;
  end;

  TBitBtnCallback = procedure (Btn: TBitBtn) is nested;
  TMemberList = specialize TFPGList<TJSONObject>;

  { TDatabaseTroopsFrame }

  TDatabaseTroopsFrame = class(TDatabaseBaseFrame)
    AddEnemyBitBtn: TBitBtn;
    TroopBattleEventFrame: TBattleEventFrame;
    EnemyHiddenCheckBox: TCheckBox;
    EnemiesListBoxPanel: TPanel;
    EnemiesListBox: TListBox;
    BattleEventGroupBox: TGroupBox;
    RemoveEnemyBitBtn: TBitBtn;
    AutoNameButton: TButton;
    EnemiesFlowPanel: TFlowPanel;
    EnemiesPaintBox: TPaintBox;
    EnemiesPaintBoxPanel: TPanel;
    EnemyButtons: TPanel;
    ClearEnemiesBitBtn: TBitBtn;
    AlignEnemiesBitBtn: TBitBtn;
    TroopImageList: TImageList;
    SetBackgroundButton: TButton;
    TestBattleButton: TButton;
    SetBackgroundPanel: TPanel;
    TestBattlePanel: TPanel;
    ContentScrollBox: TScrollBox;
    NameEdit: TEdit;
    NameLabel: TLabel;
    MainGenralSettingsFlowPanel: TFlowPanel;
    GeneralSettingsGroupBox: TGroupBox;
    NamePanel: TPanel;
    AutoNamePanel: TPanel;
    SelectorContentSplitter: TSplitter;
    TroopSelectorFrame: TItemSelectorFrame;
    procedure AddEnemyBitBtnClick(Sender: TObject);
    procedure AlignEnemiesBitBtnClick(Sender: TObject);
    procedure AutoNameButtonClick(Sender: TObject);
    procedure ClearEnemiesBitBtnClick(Sender: TObject);
    procedure EnemiesListBoxDblClick(Sender: TObject);
    procedure EnemiesPaintBoxMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure EnemiesPaintBoxMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure EnemiesPaintBoxMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure EnemiesPaintBoxPaint(Sender: TObject);
    procedure EnemyHiddenCheckBoxChange(Sender: TObject);
    procedure Init; override;
    procedure NameEditChange(Sender: TObject);
    procedure PagesTabControlChange(Sender: TObject);
    procedure RemoveEnemyBitBtnClick(Sender: TObject);
    procedure SetBackgroundButtonClick(Sender: TObject);
    function GetSortedMembers(Troop: TJSONObject): TMemberList;
  private
    IsLoading: Boolean;
    IsUpdatingHiddenCheckbox: Boolean;
    SelectedId: Integer;
    SelectedMember: TSelectedTroopMemberData;
    EnemyIsMoved: Boolean;
    EnemyImages: array of TBGRACustomBitmap;
    Background: TBGRACustomBitmap;
    EnemyImageBuffer: TBGRABitmap;

    procedure CacheEnemyImage(EnemyId: Integer);
    procedure CacheEnemyImages(Troop: TJSONObject);
    procedure UpdateBackgroud;
    function GetSelectedTroop: TJSONObject;
    function GetSelectedMember: TJSONObject;
    procedure LoadGeneralSettings(Troop: TJSONObject);
    procedure LoadBattleEvent(Troop: TJSONObject);
    function GetEnemyIdByCoordinates(X, Y: Integer): TSelectedTroopMemberData;
    procedure MoveSelectedEnemyByMouse(X, Y: Integer);
    procedure FillEnemiesListBox;
    procedure UpdateBtnsEnabled;
    procedure UpdateHiddenCheckbox;
    function IsAlignedByDefaultAlgorithm(Troop: TJSONObject): Boolean;
    procedure Align(Troop: TJSONObject);
    function GetTotalMembersWidth(Members: TJSONArray): Integer;
    function GetMemberWidth(Member: TJSONObject): Integer;
    procedure SetSameWidthForAllButtons;
    function GetScaleFactor: Double; inline;
  public
    procedure LoadTroopData(Id: Integer);
    destructor Destroy; override;
  end;

const
  DEFAULT_Y_POSITION = 436;

resourcestring
  TroopsHeader = 'Troops';

implementation

uses
  globals, gameproject, constants, doublebackgroundselection;

{$R *.lfm}

{ TDatabaseTroopsFrame }

procedure TDatabaseTroopsFrame.Init;
begin
  IsLoading := True;
  TroopSelectorFrame.SetData(Db.Troops, @Db.ResizeTroops, @LoadTroopData);
  TroopSelectorFrame.Title := TroopsHeader;
  TroopSelectorFrame.JsonDataType := 'application/rpgmv-troops';
  TroopSelectorFrame.CreateElementCallback := @Db.CreateEmptyTroop;

  SetSameWidthForAllButtons;

  UpdateBackgroud;
  EnemyImageBuffer := TBGRABitmap.Create(EnemiesPaintBox.Width, EnemiesPaintBox.Height);
  EnemyIsMoved := False;
  SelectedMember.IsActive := False;
  TroopBattleEventFrame.SetDatabase(Db);

  SetLength(EnemyImages, Db.Enemies.Count);
  FillEnemiesListBox;
  if EnemiesListBox.Count > 1 then
    EnemiesListBox.ItemIndex := 0;

  LoadTroopData(1);
end;

procedure TDatabaseTroopsFrame.NameEditChange(Sender: TObject);
var
  Troop: TJSONObject = nil;
begin
  if not IsLoading then
    Troop := GetSelectedTroop;

  if Troop <> nil then
    Troop.Strings['name'] := NameEdit.Text;
  TroopSelectorFrame.RefreshSelectedName;
end;

procedure TDatabaseTroopsFrame.PagesTabControlChange(Sender: TObject);
begin

end;

procedure TDatabaseTroopsFrame.RemoveEnemyBitBtnClick(Sender: TObject);
var
  Troop: TJSONObject;
  MemberId: Integer;
begin
  Troop := GetSelectedTroop;

  if SelectedMember.IsActive then begin
    MemberId := SelectedMember.MemberId;

    SelectedMember.IsActive := False;
    SelectedMember.MemberId := -1;

    Troop.Arrays['members'].Delete(MemberId);
    UpdateBtnsEnabled;
    UpdateHiddenCheckbox;

    EnemiesPaintBox.Refresh;
  end;
end;

procedure TDatabaseTroopsFrame.SetBackgroundButtonClick(Sender: TObject);
var
  Bg1: String = '';
  Bg2: String = '';
  Bg1Json, Bg2Json: TJSONString;
begin
  if (Db.System <> nil) and Db.System.Find('battleback1Name', Bg1Json) then
    Bg1 := Bg1Json.AsString;
  if (Db.System <> nil) and Db.System.Find('battleback2Name', Bg2Json) then
    Bg2 := Bg2Json.AsString;

  with DoubleBackgroundSelectionForm do
    if ShowBattleBgSelection(Bg1, Bg2) then begin
      Db.System.Strings['battleback1Name'] := SelectedBgs[1];
      Db.System.Strings['battleback2Name'] := SelectedBgs[2];
      UpdateBackgroud;
      EnemiesPaintBox.Refresh;
    end;
end;

function CompareMembersByY(const A, B: TJSONObject): Integer;
var
  AX: Integer = 0;
  AY: Integer = 0;
  AXJson, AYJson: TJSONNumber;
  BX: Integer = 0;
  BY: Integer = 0;
  BXJson, BYJson: TJSONNumber;
begin
  if A.Find('y', AYJson) then
    AY := AYJson.AsInteger;
  if B.Find('y', BYJson) then
    BY := BYJson.AsInteger;

  if AY = BY then begin
    if A.Find('x', AXJson) then
      AX := AXJson.AsInteger;
    if B.Find('x', BXJson) then
      BX := BXJson.AsInteger;
    if AX = BX then
      CompareMembersByY := 0
    else if AX < BX then
      CompareMembersByY := -1
    else
      CompareMembersByY := 1
  end else if AY < BY then
    CompareMembersByY := -1
  else
    CompareMembersByY := 1;
end;

function TDatabaseTroopsFrame.GetSortedMembers(Troop: TJSONObject): TMemberList;
var
  Members: TJSONArray;
  List: TMemberList;
  I: Integer;
begin
  List := TMemberList.Create;

  if Troop.Find('members', Members) then begin
    List := TMemberList.Create;
    List.Capacity := Members.Count;
    for I := 0 to Members.Count -1 do begin
      List.Add(Members.Objects[I]);
    end;
    List.Sort(@CompareMembersByY);
  end;

  GetSortedMembers := List;
end;

procedure TDatabaseTroopsFrame.EnemiesPaintBoxPaint(Sender: TObject);
var
  Troop: TJSONObject;
  Members: TJSONArray;
  Member: TJSONObject;
  EnemyId, CenterX, BottomY, LeftX, TopY: Integer;
  EnemyIdJson, CenterXJson, BottomYJson: TJSONNumber;
  IsHidden: Boolean;
  IsHiddenJson: TJSONBoolean;
  EnemyImage: TBGRACustomBitmap;
  MaskedImage, MaskImage: TBGRABitmap;
  SortedMembers: TMemberList;
  I: Integer;

  procedure FillMemberData;
  begin
    EnemyId := 0;
    if (Member <> nil) and Member.Find('enemyId', EnemyIdJson) then
      EnemyId := EnemyIdJson.AsInteger;
    CenterX := 0;
    if (Member <> nil) and Member.Find('x', CenterXJson) then
      CenterX := CenterXJson.AsInteger;
    BottomY := 0;
    if (Member <> nil) and Member.Find('y', BottomYJson) then
      BottomY := BottomYJson.AsInteger;
    IsHidden := False;
    if (Member <> nil) and Member.Find('hidden', IsHiddenJson) then
      IsHidden := IsHiddenJson.AsBoolean;

    EnemyImage := nil;
    if (EnemyId > 0) and (EnemyId <= High(EnemyImages)) then
      EnemyImage := EnemyImages[EnemyId];

    if EnemyImage <> nil then begin
      LeftX := Round(CenterX * GetScaleFactor) - EnemyImage.Width div 2;
      TopY := Round(BottomY * GetScaleFactor) - EnemyImage.Height;
    end;
  end;

begin
  if Background <> nil then
    { The shift accounts for position reserved for screen shake (the image width
      is larger than screen so that shaking wouldn't add black borders). }
    EnemyImageBuffer.CanvasBGRA.Draw(-30, 0, Background)
  else
    with EnemyImageBuffer.CanvasBGRA do begin
      Brush.Color := ColorToBGRA(clWhite);
      FillRect(0, 0, EnemiesPaintBox.Width, EnemiesPaintBox.Height);
    end;

  Troop := GetSelectedTroop;
  if (Troop <> nil) and Troop.Find('members', Members) then begin
    SortedMembers := GetSortedMembers(Troop);
    try
      for I := 0 to SortedMembers.Count -1 do begin
        Member := SortedMembers[I];
        FillMemberData;

        if EnemyImage <> nil then begin
          if IsHidden then begin
            MaskedImage := TBGRABitmap.Create(EnemyImage);
            MaskImage := TBGRABitmap.Create(EnemyImage.Width, EnemyImage.Height, BGRA(160, 160, 160));
            MaskedImage.ApplyMask(MaskImage);

            EnemyImageBuffer.CanvasBGRA.Draw(LeftX, TopY, MaskedImage);

            MaskImage.Free;
            MaskedImage.Free;
          end else
            EnemyImageBuffer.CanvasBGRA.Draw(LeftX, TopY, EnemyImage);
        end;
      end;
    finally
      SortedMembers.Free;
    end;

    for I := 0 to Members.Count -1 do begin
      Member := Members.Objects[I];
      FillMemberData;

      if SelectedMember.IsActive and (SelectedMember.MemberId = I) then
        with EnemyImageBuffer.CanvasBGRA do begin
          Pen.Color := clWhite;
          Brush.Style := bsClear;
          Rectangle(LeftX, TopY, LeftX + EnemyImage.Width, TopY + EnemyImage.Height);

          Pen.Color := clBlack;
          Rectangle(LeftX - 1, TopY - 1, LeftX + EnemyImage.Width + 1, TopY + EnemyImage.Height + 1);

          Pen.Color := clWhite;
          Rectangle(LeftX - 2, TopY - 2, LeftX + EnemyImage.Width + 2, TopY + EnemyImage.Height + 2);
        end;
    end;

  end;

  EnemyImageBuffer.Draw(EnemiesPaintBox.Canvas, 0, 0);
end;

procedure TDatabaseTroopsFrame.EnemyHiddenCheckBoxChange(Sender: TObject);
var
  Member: TJSONObject;
begin
  if IsUpdatingHiddenCheckbox then
    Exit;

  Member := GetSelectedMember;
  if Member <> nil then
    Member.Booleans['hidden'] := EnemyHiddenCheckBox.Checked;
  EnemiesPaintBox.Refresh
end;

procedure TDatabaseTroopsFrame.EnemiesPaintBoxMouseUp(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if SelectedMember.IsActive and EnemyIsMoved then begin
    MoveSelectedEnemyByMouse(X, Y);
    EnemiesPaintBox.Refresh;
  end;
  EnemyIsMoved := False;
end;

procedure TDatabaseTroopsFrame.EnemiesPaintBoxMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  SelectedMember := GetEnemyIdByCoordinates(X, Y);
  if SelectedMember.IsActive then
    EnemyIsMoved := True
  else
    EnemyIsMoved := False;
  UpdateBtnsEnabled;
  UpdateHiddenCheckbox;
  EnemiesPaintBoxPanel.SetFocus;

  EnemiesPaintBox.Refresh;
end;

procedure TDatabaseTroopsFrame.ClearEnemiesBitBtnClick(Sender: TObject);
var
  Troop: TJSONObject;
begin
  Troop := GetSelectedTroop;

  SelectedMember.IsActive := False;
  SelectedMember.MemberId := -1;

  Troop.Delete('members');
  Troop.Arrays['members'] := TJSONArray.Create;

  UpdateBtnsEnabled;
  UpdateHiddenCheckbox;

  EnemiesPaintBox.Refresh;
end;

procedure TDatabaseTroopsFrame.EnemiesListBoxDblClick(Sender: TObject);
begin
  if AddEnemyBitBtn.Enabled then
    AddEnemyBitBtn.Click;
end;

procedure TDatabaseTroopsFrame.AddEnemyBitBtnClick(Sender: TObject);
var
  Troop: TJSONObject;

  NewEnemyId: Integer;
  NewX, NewY: Integer;
  Member: TJSONObject;
  Img: TBGRACustomBitmap;
  DefaultAlignment: Boolean;
begin
  Troop := GetSelectedTroop;

  if (Troop <> nil) then begin
    DefaultAlignment := IsAlignedByDefaultAlgorithm(Troop);

    NewX := SCREEN_WIDTH div 2;
    NewY := DEFAULT_Y_POSITION;
    NewEnemyId := EnemiesListBox.ItemIndex + 1;

    CacheEnemyImage(NewEnemyId);
    Img := EnemyImages[NewEnemyId];
    if Img <> nil then
      Dec(NewX, Round(Img.Width / GetScaleFactor / 2));

    Member := TJSONObject.Create([
      'enemyId', NewEnemyId,
      'x', NewX,
      'y', NewY,
      'hidden', False
    ]);
    Troop.Arrays['members'].Add(Member);

    if DefaultAlignment then
      Align(Troop);

    UpdateBtnsEnabled;
    EnemiesPaintBox.Refresh;
  end;

end;

procedure TDatabaseTroopsFrame.AlignEnemiesBitBtnClick(Sender: TObject);
begin
  Align(GetSelectedTroop);
  EnemiesPaintBox.Refresh;
end;

procedure TDatabaseTroopsFrame.AutoNameButtonClick(Sender: TObject);
var
  Troop: TJSONObject = nil;
  Members: TJSONArray = nil;
  Enemy: TJSONObject = nil;
  NameJson: TJSONString = nil;
  Name1: String = '';
  Name2: String = '';
  Count1: Integer = 0;
  Count2: Integer = 0;
  HasMore: Boolean = False;
  I: Integer;

  function NameWithCount(N: String; C: Integer): String;
  begin
    if C = 1 then
      NameWithCount := N
    else
      NameWithCount := '%s*%d'.Format([N, C]);
  end;

var
  Res: String;
begin
  Troop := GetSelectedTroop;

  if (Troop <> nil) and Troop.Find('members', Members) then begin
    for I := 0 to Members.Count -1 do begin
      Enemy := Db.Enemies.Objects[Members.Objects[I].Integers['enemyId']];
      if Enemy.Find('name', NameJson) then begin
        if (Name1 = NameJson.AsString) and (Count1 > 0) then
          Inc(Count1)
        else if (Name2 = NameJson.AsString) and (Count2 > 0) then
          Inc(Count2)
        else if Name1 = '' then begin
          Name1 := NameJson.AsString;
          Count1 := 1;
        end else if Name2 = '' then begin
          Name2 := NameJson.AsString;
          Count2 := 1;
        end else
          HasMore := True;
      end;
    end;
  end;

  Res := NameWithCount(Name1, Count1);
  if Count2 > 0 then
    Res := Res + ', ' + NameWithCount(Name2, Count2);
  if HasMore then
    Res := Res + '...';

  NameEdit.Text := Res;
end;

procedure TDatabaseTroopsFrame.EnemiesPaintBoxMouseMove(Sender: TObject;
  Shift: TShiftState; X, Y: Integer);
begin
  if SelectedMember.IsActive and EnemyIsMoved then begin
    MoveSelectedEnemyByMouse(X, Y);
    EnemiesPaintBox.Refresh
  end;
end;

procedure TDatabaseTroopsFrame.CacheEnemyImage(EnemyId: Integer);
var
  Enemy: TJSONObject;
  EnemyFilename: String = '';
  EnemyFilenameJson: TJSONString;
  Hue: Integer = 0;
  HueJson: TJSONNumber;
  BattleStyle: TBattleStyle;
begin
  if (EnemyId >= 1) and (EnemyId < Db.Enemies.Count) then begin
    Enemy := Db.Enemies.Objects[EnemyId];
    if Enemy.Find('battlerName', EnemyFilenameJson) then
      EnemyFilename := EnemyFilenameJson.AsString;
    if Enemy.Find('battlerHue', HueJson) then
      Hue := HueJson.AsInteger;

    BattleStyle := bsFrontView;
    if Db.IsSvBattle then
      BattleStyle := bsSideView;

    EnemyImages[EnemyId] := Game.GetEnemyGraphicsBGRA(EnemyFilename, Hue,
                                                      EnemiesPaintBox.Width / SCREEN_WIDTH, BattleStyle);
  end;
end;

procedure TDatabaseTroopsFrame.CacheEnemyImages(Troop: TJSONObject);
var
    Members: TJSONArray;
    Member: TJSONObject;
    EnemyIdJson: TJSONNumber;
    I: Integer;
begin
  if Troop.Find('members', Members) then
    for I := 0 to Members.Count - 1 do begin
      Member := Members.Objects[I];
      if Member.Find('enemyId', EnemyIdJson) then
        CacheEnemyImage(EnemyIdJson.AsInteger);
    end;
end;

procedure TDatabaseTroopsFrame.UpdateBackgroud;
var
  Bg1: String = '';
  Bg2: String = '';
  Bg1Json, Bg2Json: TJSONString;
begin
  if (Db.System <> nil) and Db.System.Find('battleback1Name', Bg1Json) then
    Bg1 := Bg1Json.AsString;
  if (Db.System <> nil) and Db.System.Find('battleback2Name', Bg2Json) then
    Bg2 := Bg2Json.AsString;

  Background := Game.GetBackgroundBGRA(btBattle, Bg1, Bg2, EnemiesPaintBox.Width / SCREEN_WIDTH);
end;

function TDatabaseTroopsFrame.GetSelectedTroop: TJSONObject;
begin
  GetSelectedTroop := nil;
  if Db.Troops <> nil then
    if (SelectedId >= 1) and (SelectedId <= Db.Troops.Count -1) then
      GetSelectedTroop := Db.Troops.Objects[SelectedId];
end;

function TDatabaseTroopsFrame.GetSelectedMember: TJSONObject;
var
  Troop: TJSONObject;
  Members: TJSONArray;
begin
  GetSelectedMember := nil;

  if not SelectedMember.IsActive then
    Exit;
  if SelectedMember.MemberId < 0 then
    Exit;

  Troop := GetSelectedTroop;
  if (Troop <> nil) and Troop.Find('members', Members) then begin
    if SelectedMember.MemberId < Members.Count then
      GetSelectedMember := Members.Objects[SelectedMember.MemberId];
  end;
end;

procedure TDatabaseTroopsFrame.LoadGeneralSettings(Troop: TJSONObject);
var
  TroopName: String;
  NameJson: TJSONString;
begin
  if (Troop <> nil) and Troop.Find('name', NameJson) then
    TroopName := NameJson.AsString;
  NameEdit.Text := TroopName;
end;

procedure TDatabaseTroopsFrame.LoadBattleEvent(Troop: TJSONObject);
var
  Pages: TJSONArray = nil;
begin
  if (Troop <> nil) and Troop.Find('pages', Pages) then
    TroopBattleEventFrame.SetEditedPages(Pages, Troop);
end;

function TDatabaseTroopsFrame.GetEnemyIdByCoordinates(X, Y: Integer
  ): TSelectedTroopMemberData;
var
  Troop: TJSONObject = nil;
  Members: TJSONArray = nil;
  Member: TJSONObject = nil;
  CenterX, BottomY, LeftX, TopY, EnemyId: Integer;
  EnemyImage: TBGRACustomBitmap;
  CenterXJson, BottomYJson, EnemyIdJson: TJSONNumber;
  I: Integer;
  XMatches, YMatches: Boolean;
begin
  Troop := GetSelectedTroop;

  if (Troop <> nil) and Troop.Find('members', Members) then begin
    for I := 0 to Members.Count -1 do begin
      Member := Members.Objects[I];

      EnemyId := 0;
      if (Member <> nil) and Member.Find('enemyId', EnemyIdJson) then
        EnemyId := EnemyIdJson.AsInteger;
      CenterX := 0;
      if (Member <> nil) and Member.Find('x', CenterXJson) then
        CenterX := CenterXJson.AsInteger;
      BottomY := 0;
      if (Member <> nil) and Member.Find('y', BottomYJson) then
        BottomY := BottomYJson.AsInteger;

      EnemyImage := nil;
      if (EnemyId > 0) and (EnemyId <= High(EnemyImages)) then
        EnemyImage := EnemyImages[EnemyId];

      if EnemyImage <> nil then begin
        LeftX := Round(CenterX * GetScaleFactor) - EnemyImage.Width div 2;
        TopY := Round(BottomY * GetScaleFactor) - EnemyImage.Height;

        XMatches := (X >= LeftX) and (X <= LeftX + EnemyImage.Width);
        YMatches := (Y >= TopY) and (Y <= TopY + EnemyImage.Height);

        if XMatches and YMatches then begin
          GetEnemyIdByCoordinates.MemberId := I;
          GetEnemyIdByCoordinates.ShiftX := X - Round(CenterX * GetScaleFactor);
          GetEnemyIdByCoordinates.ShiftY := Y - Round(BottomY * GetScaleFactor);
          GetEnemyIdByCoordinates.IsActive := True;
          Exit
        end
      end
    end
  end;

  GetEnemyIdByCoordinates.MemberId := -1;
  GetEnemyIdByCoordinates.IsActive := False;
end;

procedure TDatabaseTroopsFrame.MoveSelectedEnemyByMouse(X, Y: Integer);
var
  NewX, NewY: Integer;
  Troop, Member: TJSONObject;
begin
  //TODO
  NewX := Round((X - SelectedMember.ShiftX) / GetScaleFactor);
  NewY := Round((Y - SelectedMember.ShiftY) / GetScaleFactor);

  Troop := GetSelectedTroop;
  Member := Troop.Arrays['members'].Objects[SelectedMember.MemberId];
  Member.Integers['x'] := NewX;
  Member.Integers['y'] := NewY;
end;

procedure TDatabaseTroopsFrame.FillEnemiesListBox;
var
  I: Integer;
begin
  with EnemiesListBox.Items do begin
    BeginUpdate;
    Clear;
    for I := 1 to Db.Enemies.Count -1 do begin
      Add(Db.FormatObjName(Db.Enemies.Objects[I], True));
    end;
    EndUpdate;
  end;
end;

procedure TDatabaseTroopsFrame.UpdateBtnsEnabled;
var
  Troop: TJSONObject;
  Members: TJSONArray;
  CanAdd, CanDelete, CanClear, CanAlign: Boolean;
begin
  CanAdd := False;
  CanDelete := SelectedMember.IsActive and (SelectedMember.MemberId >= 0);
  CanClear := False;
  CanAlign := False;

  Troop := GetSelectedTroop;
  if (Troop <> nil) and Troop.Find('members', Members) then begin
    CanAdd := Members.Count < 8;
    CanClear := Members.Count > 0;
    CanAlign := Members.Count > 0;
  end;

  AddEnemyBitBtn.Enabled := CanAdd;
  RemoveEnemyBitBtn.Enabled := CanDelete;
  ClearEnemiesBitBtn.Enabled := CanClear;
  AlignEnemiesBitBtn.Enabled := CanAlign;
end;

procedure TDatabaseTroopsFrame.UpdateHiddenCheckbox;
var
  Member: TJSONObject;
  IsHidden: Boolean;
  IsHiddenJson: TJSONBoolean;
begin
  IsUpdatingHiddenCheckbox := True;
  Member := GetSelectedMember;
  if (Member <> nil) and Member.Find('hidden', IsHiddenJson) then
    IsHidden := IsHiddenJson.AsBoolean;

  EnemyHiddenCheckBox.Checked := IsHidden;
  EnemyHiddenCheckBox.Enabled := Member <> nil;
  IsUpdatingHiddenCheckbox := False;
end;

function TDatabaseTroopsFrame.IsAlignedByDefaultAlgorithm(Troop: TJSONObject
  ): Boolean;
var
  Members: TJSONArray;
  TotalWidth: Integer;
  I: Integer;
  CurrectXOffset: Integer = 0;
  EveryItemOffset: Integer = 0;
  XIsAligned, YIsAligned: Boolean;

  function RelativelyEquals(A, B: Integer): Boolean;
  var
    AllowedDifference: Integer;
  begin
    { The exact difference can be different because we use downscaled images
      for comparisons, and scale factor is different depending on the system
      DPI. So, we allow things to be a few pixels off.

      Also, RPG Maker MV uses a slightly different algorithm, but this lazy
      comparison at least recognises RMMV's default placement when all the
      enemies are of the same type. }
    AllowedDifference := Round(5 / GetScaleFactor);
    RelativelyEquals := Abs(A - B) <= AllowedDifference;
  end;

begin
  Members := Troop.Arrays['members'];

  if Members.Count < 1 then begin
    IsAlignedByDefaultAlgorithm := True;
    Exit;
  end;

  YIsAligned := True;
  for I := 0 to Members.Count -1 do
    if Members.Objects[I].Integers['y'] <> DEFAULT_Y_POSITION then
      YIsAligned := False;

  TotalWidth := GetTotalMembersWidth(Members);

  if TotalWidth <= SCREEN_WIDTH then
    CurrectXOffset := (SCREEN_WIDTH - TotalWidth) div 2 + GetMemberWidth(Members.Objects[0]) div 2
  else begin
    EveryItemOffset := (TotalWidth - SCREEN_WIDTH + GetMemberWidth(Members.Objects[0]) div 2) div Members.Count;
    CurrectXOffset := GetMemberWidth(Members.Objects[0]) div 2;
  end;

  XIsAligned := True;
  for I := 0 to Members.Count -1 do begin
    if not RelativelyEquals(Members.Objects[I].Integers['x'], CurrectXOffset) then
      XIsAligned := False;
    Inc(CurrectXOffset, GetMemberWidth(Members.Objects[I]) - EveryItemOffset);
  end;

  IsAlignedByDefaultAlgorithm := XIsAligned and YIsAligned;
end;

function TDatabaseTroopsFrame.GetTotalMembersWidth(Members: TJSONArray): Integer;
var
  I: Integer;
  ImgForWidth: TBGRACustomBitmap;
  Sum: Integer;
begin
  Sum := 0;
  for I := 0 to Members.Count -1 do begin
    ImgForWidth := EnemyImages[Members.Objects[I].Integers['enemyId']];
    if ImgForWidth <> nil then
      Inc(Sum, ImgForWidth.Width);
  end;

  GetTotalMembersWidth := Round(Sum / GetScaleFactor);
end;

function TDatabaseTroopsFrame.GetMemberWidth(Member: TJSONObject): Integer;
var
  Img: TBGRACustomBitmap;
begin
  Img := EnemyImages[Member.Integers['enemyId']];
  if Img <> nil then
    GetMemberWidth := Round(Img.Width / GetScaleFactor)
  else
    GetMemberWidth := 0;
end;

procedure TDatabaseTroopsFrame.SetSameWidthForAllButtons;
  procedure DoForAllButtons(Callback: TBitBtnCallback);
  begin
    Callback(AddEnemyBitBtn);
    Callback(RemoveEnemyBitBtn);
    Callback(ClearEnemiesBitBtn);
    Callback(AlignEnemiesBitBtn);
  end;

var
  MaxWidth: Integer = 0;

  procedure FindMaxWidth(Btn: TBitBtn);
  begin
    if Btn.Width > MaxWidth then
      MaxWidth := Btn.Width;
  end;

  procedure SetMaxWidth(Btn: TBitBtn);
  begin
    Btn.AutoSize := False;
    Btn.Width := MaxWidth;
  end;

begin
  DoForAllButtons(@FindMaxWidth);
  DoForAllButtons(@SetMaxWidth);
end;

function TDatabaseTroopsFrame.GetScaleFactor: Double; inline;
begin
  GetScaleFactor := EnemiesPaintBox.Width / SCREEN_WIDTH;
end;

procedure TDatabaseTroopsFrame.Align(Troop: TJSONObject);
var
  Members: TJSONArray;
  TotalWidth: Integer;
  I: Integer;
  CurrectXOffset: Integer = 0;
  EveryItemOffset: Integer = 0;
begin
  { TODO: fix alignment so that it works well with troops of different enemy
    types. (Maybe add a second row, too?) }
  Members := Troop.Arrays['members'];

  for I := 0 to Members.Count -1 do
    Members.Objects[I].Integers['y'] := DEFAULT_Y_POSITION;

  TotalWidth := GetTotalMembersWidth(Members);

  if TotalWidth <= SCREEN_WIDTH then
    CurrectXOffset := (SCREEN_WIDTH - TotalWidth) div 2 + GetMemberWidth(Members.Objects[0]) div 2
  else begin
    EveryItemOffset := (TotalWidth - SCREEN_WIDTH + GetMemberWidth(Members.Objects[0]) div 2) div Members.Count;
    CurrectXOffset := GetMemberWidth(Members.Objects[0]) div 2;
  end;

  for I := 0 to Members.Count -1 do begin
    Members.Objects[I].Integers['x'] := CurrectXOffset;
    Inc(CurrectXOffset, GetMemberWidth(Members.Objects[I])  - EveryItemOffset);
  end;
end;

procedure TDatabaseTroopsFrame.LoadTroopData(Id: Integer);
var
  Troop: TJSONObject = nil;
begin
  IsLoading := True;
  SelectedId := Id;

  Troop := GetSelectedTroop;
  CacheEnemyImages(Troop);
  LoadGeneralSettings(Troop);
  LoadBattleEvent(Troop);

  SelectedMember.IsActive := False;
  SelectedMember.MemberId := -1;
  EnemyIsMoved := False;

  UpdateBtnsEnabled;
  UpdateHiddenCheckbox;

  EnemiesPaintBox.Refresh;

  IsLoading := False;
end;

destructor TDatabaseTroopsFrame.Destroy;
var
  I: Integer;
begin
  for I := Low(EnemyImages) to High(EnemyImages) - 1 do
    if EnemyImages[I] <> nil then begin
      EnemyImages[I].Free;
      EnemyImages[I] := nil;
    end;

  if Background <> nil then
    Background.Free;

  if EnemyImageBuffer <> nil then
    EnemyImageBuffer.Free;

  inherited Destroy;
end;

end.

